﻿using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;

namespace GenericStateMachine
{
    /// <summary>
    /// Defines the base class of state context.
    /// </summary>
    /// <typeparam name="TStateContext">The type of state context interface.</typeparam>
    /// <typeparam name="TState">The type of state interface.</typeparam>
    /// <typeparam name="TStateType">The type of concrete state enumeration.</typeparam>
    public abstract class StateContextBase<TStateContext, TState, TStateType>
        : IStateContext<TStateContext, TState, TStateType>
        where TStateContext : IStateContext<TStateContext, TState, TStateType>
        where TState : IState<TStateContext, TState, TStateType>
        where TStateType : struct, IComparable, IFormattable, IConvertible
    {
        private static Tuple<TStateType, FieldInfo, StateAttribute, StateClassForAttribute>[] GetStateMachineInfo()
        {
            var query = from v in Enum.GetValues(typeof(TStateType)).Cast<TStateType>()
                        let field = typeof(TStateType).GetField(v.ToString())
                        let state = field.GetCustomAttributes<StateAttribute>().Single()
                        let stateFor = state.StateClass.GetCustomAttribute<StateClassForAttribute>()
                        select Tuple.Create(v, field, state, stateFor);

            var ret = query.ToArray();
            return ret;
        }

        private static Type GetStateClass(TStateType stateType)
        {
            var info = GetStateMachineInfo();

            var ret = info.Single(x => EqualityComparer<TStateType>.Default.Equals(x.Item1, stateType))
                          .Item3.StateClass;
            return ret;
        }

        private static TStateType GetInitialState()
        {
            var info = GetStateMachineInfo();

            if (info.Count(x => x.Item3.StateGroup == StateGroup.Initial) != 1)
            {
                throw new InvalidOperationException("Invalid initial state.");
            }

            var ret = info.Single(x => x.Item3.StateGroup == StateGroup.Initial)
                          .Item1;

            // check attributes
            var invalidItems = info.Where(x => !EqualityComparer<TStateType>.Default.Equals(x.Item1, (TStateType)x.Item4.StateType)).ToArray();
            if (invalidItems.Any())
            {
                string details = string.Join(",", invalidItems.Select(x => string.Format("{0}-{1}", x.Item1, x.Item4.StateType)));
                string message = string.Format("StateType mismatch: {0}", details);
                throw new InvalidOperationException(message);
            }

            return ret;
        }

        
        private static readonly ConcurrentDictionary<Tuple<Type, TStateType>, Type> TypeCache = new ConcurrentDictionary<Tuple<Type, TStateType>, Type>();

        private static readonly ConcurrentDictionary<Type, TStateType> InitialStateCache = new ConcurrentDictionary<Type, TStateType>();

        private readonly ReentrancyBlocker reentrancyBlocker;

        private TState currentState;

        protected StateContextBase()
        {
            var initialState = InitialStateCache.GetOrAdd(GetType(), _ => GetInitialState());

            reentrancyBlocker = new ReentrancyBlocker();
            currentState = CreateState(initialState);
        }

        private TState CreateState(TStateType stateType)
        {
            var stateClass = TypeCache.GetOrAdd(Tuple.Create(GetType(), stateType), _ => GetStateClass(stateType));
            var ret = (TState) Activator.CreateInstance(stateClass);
            ((IStateEx)ret).SetContext(this);

            return ret;
        }

        private TResult HandleEventCore<TResult>(string eventName, Func<TState, Tuple<TStateType, TResult>> handle)
        {
            using (var scope = reentrancyBlocker.CreateScope())
            {
                var result = handle(currentState);
                var newStateType = result.Item1;
                var ret = result.Item2;

                while (!EqualityComparer<TStateType>.Default.Equals(newStateType, currentState.StateType))
                {
                    var oldState = currentState;
                    var newState = CreateState(newStateType);

                    OnStateChanging(oldState, newState, eventName);

                    oldState.OnExit();

                    currentState = newState;

                    newStateType = newState.OnEntry();

                    OnStateChanged(oldState, newState, eventName);

                    eventName = "OnEntry";
                }

                return ret;
            }
        }

        private void HandleEvent(string eventName, Func<TState, TStateType> handle)
        {
            Func<TState, Tuple<TStateType, object>> func = x => Tuple.Create<TStateType, object>(handle(x), null);

            HandleEventCore(eventName, func);
        }

        private TResult HandleEvent<TResult>(string eventName, Func<TState, Tuple<TStateType, TResult>> handle)
        {
            var ret = HandleEventCore(eventName, handle);
            return ret;
        }

        #region GetHandlerAction methods

        /// <summary>
        /// Gets the delegate for handling event.
        /// </summary>
        /// <param name="func">The method call expression.</param>
        /// <returns>The delegate for handling event.</returns>
        protected Action
            GetHandlerAction(Expression<Func<TState, TStateType>> func)
        {
            var info = CompiledExpressionCache.Get(func);

            Action ret = () => HandleEvent(info.MemberName, state => info.Invoke(state));

            return ret;
        }

        /// <summary>
        /// Gets the delegate for handling event.
        /// </summary>
        /// <param name="func">The method call expression.</param>
        /// <returns>The delegate for handling event.</returns>
        protected Action<TParam>
            GetHandlerAction<TParam>(Expression<Func<TState, TParam, TStateType>> func)
        {
            var info = CompiledExpressionCache.Get(func);

            Action<TParam> ret = (p) => HandleEvent(info.MemberName, state => info.Invoke(state, p));

            return ret;
        }

        /// <summary>
        /// Gets the delegate for handling event.
        /// </summary>
        /// <param name="func">The method call expression.</param>
        /// <returns>The delegate for handling event.</returns>
        protected Action<TParam1, TParam2>
            GetHandlerAction<TParam1, TParam2>(Expression<Func<TState, TParam1, TParam2, TStateType>> func)
        {
            var info = CompiledExpressionCache.Get(func);

            Action<TParam1, TParam2> ret = (p1, p2) => HandleEvent(info.MemberName, state => info.Invoke(state, p1, p2));

            return ret;
        }

        /// <summary>
        /// Gets the delegate for handling event.
        /// </summary>
        /// <param name="func">The method call expression.</param>
        /// <returns>The delegate for handling event.</returns>
        protected Action<TParam1, TParam2, TParam3>
            GetHandlerAction<TParam1, TParam2, TParam3>(Expression<Func<TState, TParam1, TParam2, TParam3, TStateType>> func)
        {
            var info = CompiledExpressionCache.Get(func);

            Action<TParam1, TParam2, TParam3> ret = (p1, p2, p3) => HandleEvent(info.MemberName, state => info.Invoke(state, p1, p2, p3));

            return ret;
        }

        /// <summary>
        /// Gets the delegate for handling event.
        /// </summary>
        /// <param name="func">The method call expression.</param>
        /// <returns>The delegate for handling event.</returns>
        protected Action<TParam1, TParam2, TParam3, TParam4>
            GetHandlerAction<TParam1, TParam2, TParam3, TParam4>(Expression<Func<TState, TParam1, TParam2, TParam3, TParam4, TStateType>> func)
        {
            var info = CompiledExpressionCache.Get(func);

            Action<TParam1, TParam2, TParam3, TParam4> ret = (p1, p2, p3, p4) => HandleEvent(info.MemberName, state => info.Invoke(state, p1, p2, p3, p4));

            return ret;
        }

        #endregion

        #region GetHandlerFunc methods

        /// <summary>
        /// Gets the delegate for handling event.
        /// </summary>
        /// <param name="func">The method call expression.</param>
        /// <returns>The delegate for handling event.</returns>
        protected Func<TResult>
            GetHandlerFunc<TResult>(Expression<Func<TState, Tuple<TStateType, TResult>>> func)
        {
            var info = CompiledExpressionCache.Get(func);

            Func<TResult> ret = () => HandleEvent(info.MemberName, state => info.Invoke(state));

            return ret;
        }

        /// <summary>
        /// Gets the delegate for handling event.
        /// </summary>
        /// <param name="func">The method call expression.</param>
        /// <returns>The delegate for handling event.</returns>
        protected Func<TParam, TResult>
            GetHandlerFunc<TParam, TResult>(Expression<Func<TState, TParam, Tuple<TStateType, TResult>>> func)
        {
            var info = CompiledExpressionCache.Get(func);

            Func<TParam, TResult> ret = (p) => HandleEvent(info.MemberName, state => info.Invoke(state, p));

            return ret;
        }

        /// <summary>
        /// Gets the delegate for handling event.
        /// </summary>
        /// <param name="func">The method call expression.</param>
        /// <returns>The delegate for handling event.</returns>
        protected Func<TParam1, TParam2, TResult>
            GetHandlerFunc<TParam1, TParam2, TResult>(Expression<Func<TState, TParam1, TParam2, Tuple<TStateType, TResult>>> func)
        {
            var info = CompiledExpressionCache.Get(func);

            Func<TParam1, TParam2, TResult> ret = (p1, p2) => HandleEvent(info.MemberName, state => info.Invoke(state, p1, p2));

            return ret;
        }

        /// <summary>
        /// Gets the delegate for handling event.
        /// </summary>
        /// <param name="func">The method call expression.</param>
        /// <returns>The delegate for handling event.</returns>
        protected Func<TParam1, TParam2, TParam3, TResult>
            GetHandlerFunc<TParam1, TParam2, TParam3, TResult>(Expression<Func<TState, TParam1, TParam2, TParam3, Tuple<TStateType, TResult>>> func)
        {
            var info = CompiledExpressionCache.Get(func);

            Func<TParam1, TParam2, TParam3, TResult> ret = (p1, p2, p3) => HandleEvent(info.MemberName, state => info.Invoke(state, p1, p2, p3));

            return ret;
        }

        /// <summary>
        /// Gets the delegate for handling event.
        /// </summary>
        /// <param name="func">The method call expression.</param>
        /// <returns>The delegate for handling event.</returns>
        protected Func<TParam1, TParam2, TParam3, TParam4, TResult>
            GetHandlerFunc<TParam1, TParam2, TParam3, TParam4, TResult>(Expression<Func<TState, TParam1, TParam2, TParam3, TParam4, Tuple<TStateType, TResult>>> func)
        {
            var info = CompiledExpressionCache.Get(func);

            Func<TParam1, TParam2, TParam3, TParam4, TResult> ret = (p1, p2, p3, p4) => HandleEvent(info.MemberName, state => info.Invoke(state, p1, p2, p3, p4));

            return ret;
        }

        #endregion

        /// <summary>
        /// Invoked when the state is changing.
        /// </summary>
        /// <param name="oldState">the old state.</param>
        /// <param name="newState">the new state.</param>
        /// <param name="eventName">the event name.</param>
        protected virtual void OnStateChanging(TState oldState, TState newState, string eventName)
        {
            // NOP
        }

        /// <summary>
        /// Invoked when the state is changed.
        /// </summary>
        /// <param name="oldState">the old state.</param>
        /// <param name="newState">the new state.</param>
        /// <param name="eventName">the event name.</param>
        protected virtual void OnStateChanged(TState oldState, TState newState, string eventName)
        {
            // NOP
        }

        public TStateType CurrentState
        {
            get { return currentState.StateType; }
        }

        public bool IsHandling
        {
            get { return reentrancyBlocker.IsBusy; }
        }
    }
}
